from pyxll import xl_func
from scipy.stats import norm
import math

call_option_type_list = ["Call", "call", "C", "c"]
put_option_type_list = ["Put", "put", "P", "p"]


@xl_func("string: string")
def pyFindOptionType(OptionType):
    """returns standard the option type"""
    if OptionType in call_option_type_list:
        return "C"
    if OptionType in put_option_type_list:
        return "P"
    return "Err: Option Type Not Found"


@xl_func("float FXSpot, var Delta, float IR_Dom, float IR_For, string OptionType,\
float TimeToMat, float Vol_ATM, float Vol_RR, float Vol_BF: var")
def pyGetFXVol(FXSpot, Delta, IR_Dom, IR_For, OptionType, TimeToMat, Vol_ATM, Vol_RR, Vol_BF):
    """Retrun the FX Vol from Mkt RR/BF/ATM Quote """
    OptionType = pyFindOptionType(OptionType)
    if Delta == "ATM":
        GetFXVol = Vol_ATM
    elif OptionType == "C" and Delta == 0.25:
        GetFXVol = Vol_ATM + Vol_BF + 0.5 * Vol_RR  #equation 4
    elif OptionType == "P" and Delta == 0.25:
        GetFXVol = Vol_ATM + Vol_BF - 0.5 * Vol_RR  #equation 5
    else:
        GetFXVol = "Err: Currently N/A"

    return GetFXVol

@xl_func("float FXSpot, float Strike, float vol, float IR_Dom, float IR_For, string OptionType,\
float TimeToMat, string DeltaType: var")
def pyGetFXDelta(FXSpot, Strike, vol, IR_Dom, IR_For, OptionType, TimeToMat, DeltaType):
    moneyness = math.log(FXSpot / Strike) + (IR_Dom - IR_For - 0.5 * vol **2) * TimeToMat
    moneyness = moneyness / (vol * (TimeToMat)**(1 / 2))

    OptionType = pyFindOptionType(OptionType)

    fwd = FXSpot * math.exp((IR_Dom - IR_For) * TimeToMat)

    if DeltaType == "Spot":
        if OptionType == "C":
            GetFXDelta = norm.cdf(moneyness, 0, 1) * math.exp(-IR_For * TimeToMat)
        else:
            GetFXDelta = -norm.cdf(-moneyness, 0, 1) * math.exp(-IR_For * TimeToMat)
    elif DeltaType == "FwdPremAdj" or DeltaType == "ForwardPremiumAdjusted":
        if OptionType == "C":
            GetFXDelta = norm.cdf(moneyness, 0, 1) * Strike / fwd
        else:
            GetFXDelta = -norm.cdf(-moneyness, 0, 1) * Strike / fwd
    else:
        GetFxDelta = "Err: unknown delta type"
    return GetFXDelta

####################################################################################################
####################################################################################################

# *************************************************************************************
# Functions for solving fwd prem adj delta
# Reference: Page 11 of Dimitri Reiswich's paper
# *************************************************************************************

# *************************************************************************************
# Use newton method to solve the K_min value
def solve_Kmin_target_function(FXSpot, Strike, vol, IR_Dom, IR_For, TimeToMat):
    moneyness = math.log(FXSpot / Strike) + (IR_Dom - IR_For - 0.5 * vol ** 2) * TimeToMat
    moneyness = moneyness / (vol * (TimeToMat) ** (1 / 2))
    return vol * (TimeToMat) ^ (0.5) * norm.cdf(moneyness, 0, 1) - norm.pdf(moneyness, 0, 1)


def solve_Kmin_target_function_derivative(FXSpot, Strike, vol, IR_Dom, IR_For, TimeToMat):
    moneyness = math.log(FXSpot / Strike) + (IR_Dom - IR_For - 0.5 * vol ^ 2) * TimeToMat
    moneyness = moneyness / (vol * (TimeToMat) ** (1 / 2))
    return  -1 / Strike * norm.pdf(moneyness, 0, 1) - norm.pdf(moneyness, 0, 1) * moneyness * 1 / (vol * (TimeToMat)**(1 / 2)) / Strike

def solve_Kmin(FXSpot, vol, IR_Dom, IR_For, TimeToMat, tol = 0.001):
 
    sol = FXSpot * 0.9  #initial value seems important for the root researching in this case
    #tol = 0.001

    func = solve_Kmin_target_function(FXSpot, sol, vol, IR_Dom, IR_For, TimeToMat)

    while (abs(func) > tol):
        func_derivative = solve_Kmin_target_function_derivative(FXSpot, sol, vol, IR_Dom, IR_For, TimeToMat)
        func = solve_Kmin_target_function(FXSpot, sol, vol, IR_Dom, IR_For, TimeToMat)
        sol = sol - func / func_derivative
    
    return sol






################################################################################
# Use bisection method to solve for the strike given a fwd prem adj call delta
# the delta function is monotonic on [K_min, K_Max]

def solve_strike_fwdPremAdj_Call(FXSpot, Delta, vol, IR_Dom, IR_For, TimeToMat, tol = 0.00001):

    inv_delta = norm.ppf(Delta)

    Kmax = FXSpot * math.exp((IR_Dom - IR_For + 0.5 * vol * vol) * (TimeToMat)) * math.exp(-vol * ((TimeToMat)** (1 / 2)) * inv_delta)
    KMin = solve_Kmin(FXSpot, vol, IR_Dom, IR_For, TimeToMat)

    Kup = Kmax
    Klow = KMin

    sol = 0.5 * (Kup + Klow)
    #tol = 0.00001

    func = pyGetFXDelta(FXSpot, sol, vol, IR_Dom, IR_For, "C", TimeToMat, "FwdPremAdj") - Delta

    while (abs(func) > tol):
        if func > 0:
            Klow = sol
        else:
            Kup = sol
        sol = 0.5 * (Kup + Klow)
        func = pyGetFXDelta(FXSpot, sol, vol, IR_Dom, IR_For, "C", TimeToMat, "FwdPremAdj") - Delta

    return sol


# Use bisection method to solve for the strike given a fwd prem adj put delta
# the delta function is monotonic on all K


def solve_strike_fwdPremAdj_Put(FXSpot, Delta, vol, IR_Dom, IR_For, TimeToMat, tol = 0.00001):

    alpha = 0.8 # you can export alpha as an optional param
    Kup = FXSpot * (1 + alpha)
    Klow = FSpot * (1 - alpha)

    sol = 0.5 * (Kup + Klow)
    #tol = 0.00001

    func = pyGetFXDelta(FXSpot, sol, vol, IR_Dom, IR_For, "P", TimeToMat, "FwdPremAdj") - Delta

    while (abs(func) > tol):
        if func > 0:
            Klow = sol
        else:
            Kup = sol
    
        sol = 0.5 * (Kup + Klow)
        func = pyGetFXDelta(FXSpot, sol, vol, IR_Dom, IR_For, "P", TimeToMat, "FwdPremAdj") - Delta
    

    return sol

    

############################################################################################


@xl_func("float FXSpot, float Delta, float IR_Dom, float IR_For, string OptionType,\
float TimeToMat, float VolAtStrike, string DeltaType: var")
def pyGetFxStrike(FXSpot, Delta, IR_Dom, IR_For, OptionType, TimeToMat, VolAtStrike, DeltaType = "Spot"):
    """Return the Strike given delta """
    OptionType = pyFindOptionType(OptionType)
    # for ATM option, i use the ATM - delta- neutral- convention, i.e. delta_c = - delta_put

    sqrtTimeToMat = TimeToMat ** (1 / 2)
    if DeltaType == "Spot":
        if OptionType == "C":
            alpha = - norm.ppf(Delta * Exp(IR_For * TimeToMat))
            GetFxStrike = FXSpot * math.exp(alpha * Vol_AtStrike * sqrtTimeToMat + \
            (IR_Dom - IR_For + 0.5 * Vol_AtStrike * Vol_AtStrike) * TimeToMat)
        elif OptionType == "P":
            alpha = - norm.ppf(-Delta * Exp(IR_For * TimeToMat))
            GetFxStrike = FXSpot * math.exp(-alpha * Vol_AtStrike * sqrtTimeToMat + \
            (IR_Dom - IR_For + 0.5 * Vol_AtStrike * Vol_AtStrike) * TimeToMat)
    elif DeltaType == "FwdPremAdj" or DeltaType == "ForwardPremiumAdjusted" or DeltaType == "fwdpremadj":
        if OptionType == "C":
            GetFxStrike = solve_strike_fwdPremAdj_Call(FXSpot, Delta, VolAtStrike, IR_Dom, IR_For, TimeToMat)
        elif OptionType == "P":
            GetFxStrike = solve_strike_fwdPremAdj_Put(FXSpot, Delta, VolAtStrike, IR_Dom, IR_For, TimeToMat)
    else:
        GetFxStrike = "Err: Wrong Delta Type"

    return GetFxStrike


@xl_func("float FXSpot, var Delta, float IR_Dom, float IR_For, string OptionType,\
float TimeToMat, float Vol_ATM, float Vol_RR, float Vol_BF, string DeltaType: var")
def pyGetFxStrikeFromMktQuote(FXSpot, Delta, IR_Dom, IR_For, OptionType, TimeToMat, \
 Vol_ATM, Vol_RR, Vol_BF, DeltaType = "Spot"):

    OptionType = pyFindOptionType(OptionType)
    # for ATM option, i use the ATM - delta- neutral- convention, i.e. delta_c = - delta_put

    if DeltaType == "Spot":
        alpha = - norm.ppf(0.25 * math.exp(IR_For) * TimeToMat)
        Vol_AtStrike = pyGetFXVol(FXSpot, Delta, IR_Dom, IR_For, OptionType, TimeToMat, Vol_ATM, Vol_RR, Vol_BF)
          
        if OptionType == "C" and Delta == 0.25:
            GetFxStrikeFromMktQuote = FXSpot * math.exp(alpha * Vol_AtStrike * (TimeToMat) ** (1 / 2) + _
            (IR_Dom - IR_For + 0.5 * Vol_AtStrike ** 2) * TimeToMat) #equation 7
        elif OptionType == "P" and Delta == -0.25:
            GetFxStrikeFromMktQuote = FXSpot * math.exp(-alpha * Vol_AtStrike * (TimeToMat) ** (1 / 2) + _
            (IR_Dom - IR_For + 0.5 * Vol_AtStrike ** 2) * TimeToMat) #equation 6
        elif Delta == "ATM":
            GetFxStrikeFromMktQuote = FXSpot * math.exp((IR_Dom - IR_For + 0.5 * sigma_ATM **2) * TimeToMat) #equation 1
        else:
            GetFxStrikeFromMktQuote = "Err: Not a Market Quote"
    elif DeltaType == "FwdPremAdj" or DeltaType == "ForwardPremiumAdjusted" or DeltaType == "fwdpremadj":
        if Delta == "ATM":
            GetFxStrikeFromMktQuote = FXSpot * math.exp((IR_Dom - IR_For + 0.5 * Vol_ATM ** 2) * TimeToMat)
        elif OptionType == "C" and Delta == 0.25:
            Vol_25DeltaCall = pyGetFXVol(FXSpot, 25 / 100, IR_Dom, IR_For, "C", TimeToMat, Vol_ATM, Vol_RR, Vol_BF)
            GetFxStrikeFromMktQuote = solve_strike_fwdPremAdj_Call(FXSpot, Delta, Vol_25DeltaCall, IR_Dom, IR_For, TimeToMat)
        elif OptionType == "P" and Delta == -0.25:
            Vol_25DeltaPut = pyGetFXVol(FXSpot, 25 / 100, IR_Dom, IR_For, "P", TimeToMat, Vol_ATM, Vol_RR, Vol_BF)
            GetFxStrikeFromMktQuote = solve_strike_fwdPremAdj_Put(FXSpot, Delta, Vol_25DeltaPut, IR_Dom, IR_For, TimeToMat)
	else:
	    GetTXStrikeFromMktQuote = "Err: Delta Type is currently not recognized"

    return GetFxStrikeFromMktQuote
